/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, MatcherTypes, StmtMatcher, StmtMatcherCallback, IssueReport, CheckerUtils } from '../../Index';
import { ArkInstanceInvokeExpr, ArkInvokeStmt, FunctionType, Local, Stmt, Constant, ArkStaticInvokeExpr, ArkAssignStmt, ArkMethod, Type } from 'arkanalyzer';
import { MethodParameter } from 'arkanalyzer/lib/core/model/builder/ArkMethodBuilder';

const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/window-size-change-listener-check.md',
    description: 'Enable listening for window size changes when possible.'
};

const windowClassSignature = `@ohosSdk/api/@ohos.window.d.ts: window.Window`;

function isWindowClassType(type: Type | undefined): boolean {
    if (!type) {
        return false;
    }
    const typeStr = type.getTypeString && type.getTypeString();
    return typeof typeStr === 'string' && typeStr.includes('window.Window');
}

export class WindowSizeChangeListenerCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private stmtMatcher: StmtMatcher = {
        matcherType: MatcherTypes.STMT,
        match(stmt: Stmt): boolean {
            // 只匹配 window.createWindow 调用
            if (!(stmt instanceof ArkInvokeStmt)) {
                return false;
            }
            const invokeExpr = stmt.getInvokeExpr();
            if (!(invokeExpr instanceof ArkStaticInvokeExpr)) {
                return false;
            }
            const methodSig = invokeExpr.getMethodSignature().toString();
            return methodSig.includes('createWindow');
        }
    };

    public registerMatchers(): StmtMatcherCallback[] {
        return [{ matcher: this.stmtMatcher, callback: this.check }];
    }

    public check = (stmt: Stmt): void => {
        if (this.hasGlobalOnWindowSizeChange(stmt)) {
            return;
        }
        this.checkCreateWindowCallback(stmt);
    };

    private isWindowOnWindowSizeChangeInvoke(stmt: Stmt): boolean {
        const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
        if (!(invokeExpr instanceof ArkInstanceInvokeExpr)) {
            return false;
        }
        if (!this.isOnMethod(invokeExpr)) {
            return false;
        }
        if (!this.isWindowInstance(invokeExpr)) {
            return false;
        }
        return this.isWindowSizeChangeArg(invokeExpr);
    }

    private isOnMethod(invokeExpr: ArkInstanceInvokeExpr): boolean {
        const methodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();
        return methodName === 'on';
    }

    private isWindowInstance(invokeExpr: ArkInstanceInvokeExpr): boolean {
        const base = invokeExpr.getBase();
        return !!base && isWindowClassType(base.getType());
    }

    private isWindowSizeChangeArg(invokeExpr: ArkInstanceInvokeExpr): boolean {
        const args = invokeExpr.getArgs();
        if (args.length === 0) {
            return false;
        }
        const arg0 = args[0];
        const argVal = (arg0 instanceof Constant) ? arg0.getValue() : undefined;
        return argVal === 'windowSizeChange';
    }

    private hasGlobalOnWindowSizeChange(stmt: Stmt): boolean {
        const methodCfg = stmt.getCfg();
        const allStmts = methodCfg?.getStmts() ?? [];
        for (const s of allStmts) {
            if (this.isWindowOnWindowSizeChangeInvoke(s)) {
                return true;
            }
        }
        return false;
    }

    private checkCreateWindowCallback(stmt: Stmt): void {
        const invokeExpr = stmt.getInvokeExpr();
        if (!(invokeExpr instanceof ArkStaticInvokeExpr)) {
            return;
        }
        const args = invokeExpr.getArgs();
        for (const arg of args) {
            if (!this.isFunctionType(arg)) {
                continue;
            }
            const funcType = arg.getType() as FunctionType;
            const params = this.getCallbackParams(funcType);
            for (let i = 0; i < params.length; i++) {
                const param = params[i];
                if (!isWindowClassType(param.getType())) {
                    continue;
                }
                const windowParamName = param.getName();
                if (this.callbackHasOnWindowSizeChange(stmt, funcType, windowParamName)) {
                    return;
                }
                if (arg instanceof Local) {
                    this.reportIssue(stmt, args[0] as Local, windowParamName);
                }
            }
        }
    }

    private isFunctionType(arg: unknown): arg is Local {
        return !!arg && typeof (arg as Local).getType === 'function' && (arg as Local).getType() instanceof FunctionType;
    }

    private getCallbackParams(funcType: FunctionType): MethodParameter[] {
        const subSignature = funcType.getMethodSignature().getMethodSubSignature();
        return subSignature.getParameters();
    }

    private callbackHasOnWindowSizeChange(stmt: Stmt, funcType: FunctionType, windowParamName: string): boolean {
        const scene = stmt.getCfg()?.getDeclaringMethod()?.getDeclaringArkClass()?.getDeclaringArkFile()?.getScene?.();
        const arkMethod = scene && scene.getMethod && scene.getMethod(funcType.getMethodSignature());
        return !!(arkMethod && this.hasOnWindowSizeChangeCall(arkMethod, windowParamName));
    }

    private hasOnWindowSizeChangeCall(arkMethod: ArkMethod, windowParamName: string): boolean {
        const stmts = arkMethod.getBody()?.getCfg()?.getStmts() ?? [];
        for (const stmt of stmts) {
            if (this.isWindowOnWindowSizeChangeInvoke(stmt)) {
                return true;
            }
        }
        return false;
    }

    private reportIssue(stmt: Stmt, arg: Local, varName: string): void {
        const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
        let fullPosition = stmt.getOperandOriginalPosition(arg);
        if (!fullPosition) {
            return;
        }
        let lineNum = fullPosition.getFirstLine();
        const keyword = 'createWindow(';
        let startColumn = fullPosition.getFirstCol() - keyword.length;
        let endColumn = fullPosition.getFirstCol() - 1;

        if (lineNum === -1 || startColumn === -1 || endColumn === -1) {
            return;
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        let defects = new Defects(lineNum, startColumn, endColumn, this.metaData.description, severity,
            this.rule.ruleId, filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}





